package com.marshalchen.common.uiModule.flowtextview;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
import com.marshalchen.common.uiModule.flowtextview.helpers.ClickHandler;
import com.marshalchen.common.uiModule.flowtextview.helpers.CollisionHelper;
import com.marshalchen.common.uiModule.flowtextview.helpers.PaintHelper;
import com.marshalchen.common.uiModule.flowtextview.helpers.SpanParser;
import com.marshalchen.common.uiModule.flowtextview.listeners.OnLinkClickListener;
import com.marshalchen.common.uiModule.flowtextview.models.HtmlLink;
import com.marshalchen.common.uiModule.flowtextview.models.HtmlObject;
import com.marshalchen.common.uiModule.flowtextview.models.Line;
import com.marshalchen.common.uiModule.flowtextview.models.Obstacle;

import java.util.ArrayList;


public class FlowTextView extends RelativeLayout {

    // FIELDS
    private final PaintHelper mPaintHelper = new PaintHelper();
    private final SpanParser mSpanParser = new SpanParser(this, mPaintHelper);
    private final ClickHandler mClickHandler = new ClickHandler(mSpanParser);
    private int mColor = Color.BLACK;
    private int pageHeight = 0;
    private TextPaint mTextPaint;
    private TextPaint mLinkPaint;
    private float mTextsize = 20.0f;
    private Typeface typeFace;
    private int mDesiredHeight = 100; // height of the whole view
    private boolean needsMeasure = true;
    private final ArrayList<Obstacle> obstacles = new ArrayList<Obstacle>();
    private CharSequence mText = "";
    private boolean mIsHtml = false;

	public FlowTextView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init(context);
	}

	public FlowTextView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context);
	}

	public FlowTextView(Context context) {
		super(context);
		init(context);
	}

	private void init(Context context){
		mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
		mTextPaint.density = getResources().getDisplayMetrics().density;		
		mTextPaint.setTextSize(mTextsize);
		mTextPaint.setColor(Color.BLACK);
		mLinkPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
		mLinkPaint.density = getResources().getDisplayMetrics().density;		
		mLinkPaint.setTextSize(mTextsize);
		mLinkPaint.setColor(Color.BLUE);
		mLinkPaint.setUnderlineText(true);
		this.setBackgroundColor(Color.TRANSPARENT);
		this.setOnTouchListener(mClickHandler);
	}

    // INTERESTING DRAWING STUFF
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float mViewWidth = this.getWidth();
        obstacles.clear(); // clear old data, boxes stores an array of "obstacles" that we need to paint the text around
        int lowestYCoord = findBoxesAndReturnLowestObstacleYCoord(); // find the "obstacles" within the view and get the lowest obstacle coordinate at the same time
        String[] blocks = mText.toString().split("\n"); // split the text into its natural blocks


        // set up some counter and helper variables we will us to traverse through the string to be rendered
        int charOffsetStart = 0; // tells us where we are in the original string
        int charOffsetEnd = 0; // tells us where we are in the original string
        int lineIndex = 0;
        float xOffset; // left margin off a given line
        float maxWidth; // how far to the right it can strectch
        float yOffset = 0;
        String thisLineStr; // the current line we are trying to render
        int chunkSize;
        int lineHeight = getLineHeight(); // get the height in pixels of a line for our current TextPaint

        ArrayList<HtmlObject> lineObjects = new ArrayList<HtmlObject>(); // this will get populated with special html objects we need to render
        Object[] spans;

        HtmlObject htmlLine;// reuse for single plain lines

        mSpanParser.reset();

        for(int block_no = 0; block_no <= blocks.length-1; block_no++) // at the highest level we iterate through each 'block' of text
        {
            String thisBlock = blocks[block_no];
            if(thisBlock.length()<=0){ //is a line break
                lineIndex++; // we need a new line
                charOffsetEnd += 2;
                charOffsetStart = charOffsetEnd;
            }else{ // is some actual text

                while(thisBlock.length()>0){ // churn through the block spitting it out onto seperate lines until there is nothing left to render
                    lineIndex++; // we need a new line
                    yOffset = lineIndex * lineHeight; // calculate our new y position based on number of lines * line height
                    Line thisLine = CollisionHelper.calculateLineSpaceForGivenYOffset(yOffset, lineHeight, mViewWidth, obstacles); // calculate a theoretical "line" space that we have to paint into based on the "obstacles" that exist at this yOffset and this line height - collision detection essentially
                    xOffset = thisLine.leftBound;
                    maxWidth = thisLine.rightBound - thisLine.leftBound;
                    float actualWidth;



                    // now we have a line of known maximum width that we can render to, figure out how many characters we can use to get that width taking into account html funkyness
                    do {
                        chunkSize = getChunk(thisBlock, maxWidth);
                        int thisCharOffset = charOffsetEnd+chunkSize;

                        if(chunkSize>1){
                            thisLineStr = thisBlock.substring(0, chunkSize);
                        }
                        else{
                            thisLineStr = "";
                        }

                        lineObjects.clear();

                        if(mIsHtml){
                            spans = ((Spanned) mText).getSpans(charOffsetStart,  thisCharOffset, Object.class);
                            if(spans.length > 0){
                                actualWidth = mSpanParser.parseSpans(lineObjects, spans, charOffsetStart, thisCharOffset, xOffset);
                            }else{
                                actualWidth = maxWidth; // if no spans then the actual width will be <= maxwidth anyway
                            }
                        }else{
                            actualWidth = maxWidth;// if not html then the actual width will be <= maxwidth anyway
                        }

                        if(actualWidth>maxWidth){
                            maxWidth-=5; // if we end up looping - start slicing chars off till we get a suitable size
                        }

                    } while (actualWidth > maxWidth);

                    // chunk is ok
                    charOffsetEnd += chunkSize;


                    if(lineObjects.size() <= 0 ){ // no funky objects found, add the whole chunk as one object
                        htmlLine = new HtmlObject(thisLineStr, 0, 0, xOffset, mTextPaint);
                        lineObjects.add(htmlLine);
                    }

                    for (HtmlObject thisHtmlObject : lineObjects) {

                        if(thisHtmlObject instanceof HtmlLink){
                            HtmlLink thisLink = (HtmlLink) thisHtmlObject;
                            float thisLinkWidth = thisLink.paint.measureText(thisHtmlObject.content);
                            mSpanParser.addLink(thisLink, yOffset, thisLinkWidth, lineHeight);
                        }

                        paintObject(canvas, thisHtmlObject.content, thisHtmlObject.xOffset, yOffset, thisHtmlObject.paint);

                        if(thisHtmlObject.recycle){
                            mPaintHelper.recyclePaint(thisHtmlObject.paint);
                        }
                    }


                    if(chunkSize>=1) thisBlock = thisBlock.substring(chunkSize, thisBlock.length());

                    charOffsetStart = charOffsetEnd;
                }
            }
        }

        yOffset += (lineHeight/2);

        View child = getChildAt(getChildCount()-1);
        if (child.getTag() != null)
        {
            if (child.getTag().toString().equalsIgnoreCase("hideable"))
            {
                if (yOffset > pageHeight)
                {
                    if (yOffset < obstacles.get(obstacles.size()-1).topLefty - getLineHeight())
                    {
                        child.setVisibility(View.GONE);
                    }
                    else
                    {
                        child.setVisibility(View.VISIBLE);
                    }
                }
                else
                {
                    child.setVisibility(View.GONE);
                }
            }
        }

        mDesiredHeight = Math.max(lowestYCoord, (int) yOffset);
        if(needsMeasure){
            needsMeasure = false;
            requestLayout();
        }
    }
    private int findBoxesAndReturnLowestObstacleYCoord(){
        int lowestYCoord = 0;
        int childCount = this.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != View.GONE)
            {
                Obstacle obstacle = new Obstacle();
                obstacle.topLeftx = child.getLeft();
                obstacle.topLefty = child.getTop();
                obstacle.bottomRightx = obstacle.topLeftx + child.getWidth();
                obstacle.bottomRighty = obstacle.topLefty + child.getHeight();
                obstacles.add(obstacle);
                if(obstacle.bottomRighty > lowestYCoord) lowestYCoord = obstacle.bottomRighty;
            }
        }

        return lowestYCoord;
    }
	private int getChunk(String text, float maxWidth){		
		int length = mTextPaint.breakText(text, true, maxWidth, null);
		if(length<=0) return length; // if its 0 or less, return it, can't fit any chars on this line
		else if(length>=text.length()) return length; // we can fit the whole string in
		else if(text.charAt(length-1) == ' ') return length; // if break char is a space  -- return
		else{
			if(text.length() > length)	if(text.charAt(length) == ' ') return length + 1; // or if the following char is a space then return this length - it is fine
		}		 

		// otherwise, count back until we hit a space and return that as the break length
		int tempLength = length-1;
		while(text.charAt(tempLength)!= ' '){
			tempLength--;
			if(tempLength <=0) return length; // if we count all the way back to 0 then this line cannot be broken, just return the original break length
		}		

		return tempLength+1; // return the nicer break length which doesn't split a word up

	}
	private void paintObject(Canvas canvas, String thisLineStr, float xOffset, float yOffset, Paint paint){
		canvas.drawText(thisLineStr, xOffset, yOffset, paint);
	}

    // MINOR VIEW EVENTS
    @Override
    protected void onConfigurationChanged(Configuration newConfig)
    {
        super.onConfigurationChanged(newConfig);
        this.invalidate();
    }
    @Override
    public void invalidate() {
        this.needsMeasure = true;
        super.invalidate();
    }
    @Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);	

		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		int widthSize = MeasureSpec.getSize(widthMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);

		int width;
		int height;

		if (widthMode == MeasureSpec.EXACTLY) {
			// Parent has told us how big to be. So be it.
			width = widthSize;
		} else {	
			width = this.getWidth();		
		}

		if (heightMode == MeasureSpec.EXACTLY) {
			// Parent has told us how big to be. So be it.
			height = heightSize;           
		} else {
			height = mDesiredHeight;
		}

		setMeasuredDimension(width, height + getLineHeight());
	}

    // GETTERS AND SETTERS
    // text size
    public float getTextsize() {
        return mTextsize;
    }
    public void setTextSize(float textSize){
        this.mTextsize = textSize;
        mTextPaint.setTextSize(mTextsize);
        mLinkPaint.setTextSize(mTextsize);
        invalidate();
    }

    // typeface
    public Typeface getTypeFace() {
        return typeFace;
    }
    public void setTypeface(Typeface type){
        this.typeFace = type;
        mTextPaint.setTypeface(typeFace);
        mLinkPaint.setTypeface(typeFace);
        invalidate();
    }

    // text paint
    public TextPaint getTextPaint() {
        return mTextPaint;
    }
    public void setTextPaint(TextPaint mTextPaint) {
        this.mTextPaint = mTextPaint;
        invalidate();
    }

    // link paint
    public TextPaint getLinkPaint() {
        return mLinkPaint;
    }
    public void setLinkPaint(TextPaint mLinkPaint) {
        this.mLinkPaint = mLinkPaint;
        invalidate();
    }

    // text content
    public CharSequence getText() {
        return mText;
    }
    public void setText(CharSequence text){
        mText = text;
        if(text instanceof Spannable){
            mIsHtml = true;
            mSpanParser.setSpannable((Spannable) text);
        }else{
            mIsHtml = false;
        }
        this.invalidate();
    }

    // text colour
    public int getColor() {
        return mColor;
    }
    public void setColor(int color){
        this.mColor = color;

        if(mTextPaint!=null){
            mTextPaint.setColor(mColor);
        }

        mPaintHelper.setColor(mColor);

        this.invalidate();
    }

    // link click listener
    public OnLinkClickListener getOnLinkClickListener() {
        return mClickHandler.getOnLinkClickListener();
    }
    public void setOnLinkClickListener(OnLinkClickListener onLinkClickListener){
        mClickHandler.setOnLinkClickListener(onLinkClickListener);
    }

    // line height
    public int getLineHeight() {
        float mSpacingMult = 1.0f;
        float mSpacingAdd = 0.0f;
        return Math.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult
                + mSpacingAdd);
    }

    // page height
    public void setPageHeight(int pageHeight)
    {
        this.pageHeight = pageHeight;
        invalidate();
    }
}